/*
 * Copyright 2010-2011 Marcin Blazejewski
 *
 * This file is part of Ground0.
 *
 * Ground0 is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Ground0 is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Ground0. If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <config.h>
#include "world.hpp"
#include "camera.hpp"
#include "wloader.hpp"
#include "properties.hpp"

#include <GL/gl.h>
#include <GL/glu.h>
#include <SDL/SDL.h>
#include <time.h>
#include <math.h>
#include <vector>

#include <iostream>


using namespace std;

#define GG  64
#define SCALE 25
//#define SCALE 16
//#define SECSIZE ((GG-2) * SCALE)

void dumpmatrix(float* m) {
    if (!m) { cout << "null matrix" << endl; return; }
    cout << "| " << m[0] << ", " << m[1] << ", " << m[2] << ", " << m[3] << endl;
    cout << "| " << m[4] << ", " << m[5] << ", " << m[6] << ", " << m[7] << endl;
    cout << "| " << m[8] << ", " << m[9] << ", " << m[10] << ", " << m[11] << endl;
    cout << "| " << m[12] << ", " << m[13] << ", " << m[14] << ", " << m[15] << endl;
}


void load_rules(class Properties &config) {
    ROTVEL_CAP = config.get_float("rotvel.cap", ROTVEL_CAP);
    ROT_ACCEL = config.get_float("rot.accel", ROT_ACCEL);
    AUTO_HEADING_SCALE = config.get_int("auto_heading.scale", AUTO_HEADING_SCALE);
    AUTO_HEADING_LEVELS = config.get_int("auto_heading.levels", AUTO_HEADING_LEVELS);

    AMBIENT_LIGHT  = config.get_float("light.ambient", AMBIENT_LIGHT);
    DIFFUSE_LIGHT  = config.get_float("light.diffuse", DIFFUSE_LIGHT);

    BULLET_SPEED  = config.get_int("bullet.speed", BULLET_SPEED);
    GUN_COOLDOWN = config.get_int("gun.cooldown", GUN_COOLDOWN);

    ASTEROIDS_N = config.get_int("asteroids.n", ASTEROIDS_N);
    ASTEROIDS_BASE_RADIUS = config.get_int("asteroids.base_radius", ASTEROIDS_BASE_RADIUS);
    ASTEROIDS_SPAN = config.get_int("asteroids.span", ASTEROIDS_SPAN);
    ASTEROIDS_SPEED = config.get_int("asteroids.speed", ASTEROIDS_SPEED);

    ASTEROIDS_CX = config.get_int("asteroids.center.x", ASTEROIDS_CX);
    ASTEROIDS_CY = config.get_int("asteroids.center.y", ASTEROIDS_CY);
    ASTEROIDS_CZ = config.get_int("asteroids.center.y", ASTEROIDS_CZ);
}

/******************************************************************************/
/* World */
/******************************************************************************/

World::World() {
    s1 = NULL;

    ambient.set_color(AMBIENT_LIGHT, AMBIENT_LIGHT, AMBIENT_LIGHT);
    lights[0].set_color(DIFFUSE_LIGHT, DIFFUSE_LIGHT, DIFFUSE_LIGHT);
    lights[0].move( -0.5f * SECSIZE,  3.5f * SCALE, -0.5f * SECSIZE);
    lights[1].on = false;
    lights[2].on = false;
    lights[3].on = false;

    int x, y, z;
    int sx, sy;
    
    /* STARS */
    float r = 500;
    for (int i = 0; i < N_STARS; i++) {
        float a = (rand() % 628) / 100.0f;
        float b = (rand() % 628) / 100.0f;

        float t = r * cos(b);

        stars[i].y = r * sin(b);
        stars[i].x = t * sin(a);
        stars[i].z = t * cos(a);
    }


    memset(abp, 0, sizeof(abp));
    /* asteroids */
    int n = N_ASTEROID_MODELS;
    abp[0] = new Stuff(get_model("a1"));
    abp[1] = new Stuff(get_model("a2"));
    abp[2] = new Stuff(get_model("a3"));
    abp[3] = new Stuff(get_model("a4"));
    for (int i=0;i<n;i++) {
        abp[i]->calc_cr();
    }

    float step = 2 * M_PI / ASTEROIDS_N;
    grav = Vector(ASTEROIDS_CX, ASTEROIDS_CY, ASTEROIDS_CZ);
    for (float a = 0; a < 2*M_PI; a+=step) {
        int bpi = rand() % n;
        Asteroid *ast = new Asteroid( abp[bpi] );
        ast->use_grav = true;

        float r = ASTEROIDS_BASE_RADIUS + (rand() % ASTEROIDS_SPAN);
        float el = 40 - (rand() % 40);
        el = el / 360 * 2 * M_PI;

        ast->pos = Vector::from_polar(r, a, el);

        ast->randomize();
        ast->pos += grav;

        ast->velocity = Vector::from_polar(1, a + M_PI_2, 0);
        ast->velocity *= ASTEROIDS_SPEED;

        stuff.push_back(ast);
        update_collision_cache(ast);
    }


    generate_track();

    cout << "[debug ] sectors in collision cache: " << cache.size() << endl;

    map<int, list<Stuff*> >::iterator ci;
    int max = 0;
    for (ci = cache.begin(); ci != cache.end(); ci++) {
        int cnt = ci->second.size();
        if (cnt > max) max = cnt;
    }
    cout << "[debug ] max objects in sector: " << max << endl;

    camera = new Camera();    

    relight();    
}

World::~World() {
    if (s1) delete s1;

    for (int i = 0; i < N_ASTEROID_MODELS; i++)
        if (abp[i]) delete abp[i];

    if (camera) delete camera;
    /// @todo cleanup all objects
}

void World::generate_track() {
    Stuff *gate = NULL;

    for (int i = 0; i < 10; i++) {
        gate = new Stuff(get_model("gate"));
        gate->rigid = true;
        //gate->set_cm(get_model("gate-c"));
        gate->calc_cr();
        gate->move(0, 20, 80 * i);
        gate->orientation = Quaternion(M_PI/2.0, Vector::x_axis);
        
        stuff.push_back(gate);
        gates.push_back(gate);
    }

    next_gate = 0;

}

Stuff* World::get_next_gate() {
    return gates[next_gate];
}

void World::set_ship_model(Block *blueprint) {
    if (!s1) {
        s1 = new Sim(blueprint);
        //s1->set_cm(get_model("s1-c"));
        //s1->update_cm();
        stuff.push_back(s1);
    } else {
        //s1->blocks = blueprint->blocks;
        s1->block = blueprint;
    }
    s1->calc_cr();
}

void World::set_camera_mode(int m) {
    switch (m) {
    case 0: camera = new ChaseCam(*camera, s1); break;
    case 1: camera = new RearCam(*camera, s1); break;
    case 2: camera = new TopCam(*camera, s1); break;
    case 3: camera = new FixedCam(*camera, s1); break;
    }
}
 
 
/** Update collision cache and object's collision cache index.
 * If the object has moved to a different collision cache sector than
 * its cidx fields points to, remove it from previous sector, 
 * add to current sector and update its collision index.
 *
 * @param s object to update
 */
void World::update_collision_cache(Stuff *s) {
    // calculate objects collision cache index
    signed char cx = (signed char)(s->pos.x / SECSIZE);
    signed char cy = (signed char)(s->pos.y / SECSIZE);
    signed char cz = (signed char)(s->pos.z / SECSIZE);
    int c = (cx << 16) | (cy << 8) | cz;

    // check if its different than before
    if (c != s->cidx) {
        // remove obj from old previous sector list
        cache[s->cidx].remove(s);
        // add to new sector
        s->cidx = c;
        cache[s->cidx].push_back(s);
    }
    
}

void World::tick(unsigned int ms) {    
    float dt = ms / 1000.f;
    list<Stuff*>::iterator i, j;
    signed char cx;
    signed char cy;
    signed char cz;
    int c;

    /* the camera */
    camera->tick(ms);

    /* objects */
    
    for (i = stuff.begin(); i != stuff.end(); i++) {
        // dirty temporary cheap effect:
        // asteroids move around grav center
        if ((*i)->use_grav) {
            Vector a = grav - (*i)->pos;
            float r = a.length();
            a.normalize();
            a *= ASTEROIDS_SPEED * ASTEROIDS_SPEED / r;
            (*i)->velocity += (a * dt);
        }     

        (*i)->tick(dt);
        update_collision_cache(*i);
    }
    
    /* collisions */

    map<int, list<Stuff*> >::iterator ci;
    Stuff *a, *b;
    static Vector n, p1, p2;

    // for each collision cache ...
    for (ci = cache.begin(); ci != cache.end(); ci++) {
        // for each object in this cache ...
        for (i = ci->second.begin(); i != ci->second.end(); i++) {
            // check if this object...
            a = *i;
            j = i;
            //  ... collides with any other from this sector
            for (j++; j != ci->second.end(); j++) {
                b = *j;

                
                if (a->collides_with(b, &n, &p1, &p2)) {
                    //collide(a, b);
                    //Stuff::collide(a, b, n, p1, p2, dt);
                    a->dyn.collide(*a, *b, n, p1, p2, dt);
                    

                    if ((a == s1) || (b == s1)) {
                        s1->collision_recovery = s1->velocity.length() * 50;
                        if (s1->collision_recovery > 0) {
                            s1->auto_heading = 0.1;
                        }
                    }
                }


            }
        }
    }


    /* bullet collisions */

    list<Particle>::iterator bi;
    for (bi = s1->bullets.particles.begin(); bi != s1->bullets.particles.end(); bi++) {
        cx = (signed char)(bi->x / SECSIZE);
        cy = (signed char)(bi->y / SECSIZE);
        cz = (signed char)(bi->z / SECSIZE);
        c = (cx << 16) | (cy << 8) | cz;
        
        ci = cache.find(c);
        if (ci == cache.end()) continue;

        for (i = ci->second.begin(); i != ci->second.end(); i=j) {
            Vector d(bi->x, bi->y, bi->z);
            d -= (*i)->pos;
 
            j = i;
            j++;
            if (d.length() < ((*i)->cR * 0.66)) {
                if (--((*i)->life) <= 0) {
                    ci->second.erase(i);
                    bullet_hit(*bi, *i);
                }
            }
        }
    }
}


void World::render() {
    list<Stuff*>::iterator i;

    camera->apply();
    glPushMatrix();

    // stars
    glDisable(GL_DEPTH_TEST);
    glDisable(GL_LIGHTING);
    glDisable(GL_TEXTURE_2D);
    glDisable(GL_BLEND);
    glTranslatef( camera->get_pos().x, camera->get_pos().y, camera->get_pos().z );
    glColor3f(1, 1, 1);
    glBindTexture(GL_TEXTURE_2D, 0);

    glBegin(GL_POINTS);
    for (int j = 0; j < N_STARS; j++) {
        glVertex3f(stars[j].x, stars[j].y, stars[j].z);
    }
    glEnd();

    glEnable(GL_DEPTH_TEST);
    glEnable(GL_LIGHTING);
    glEnable(GL_TEXTURE_2D);

    // lights
    render_light();

    // draw shapes
    for (i = stuff.begin(); i != stuff.end(); i++) {
        // reset to camera matrix
        glPopMatrix();
        glPushMatrix();

        (*i)->draw();
    }
    
    // bullets
    glPopMatrix();
    glPushMatrix();

    glColor4f(0.9, 0.4, 0, 1);
    glDisable(GL_TEXTURE_2D);
    glDisable(GL_DEPTH_TEST);
    glDisable(GL_LIGHTING);

    s1->bullets.draw();

    glPopMatrix();
}


#define VEX    16.0
void World::bullet_hit(Particle &bullet, Stuff *target) {
    bullet.life = 0;

    stuff.remove(target);
    if (target->cR < 3) return;

    float SPR = target->cR * 0.6 + 1;

    for (int i = 0; i < 5; i++) {
        Asteroid *debris = new Asteroid( (Asteroid&)*target );
        debris->randomize(R_ORIENTATION | R_ROTATION);
        debris->pos = target->pos;

        switch (i) {
        case 0:
            debris->pos += Vector(SPR, 0, 0);
            debris->velocity = Vector(VEX, 0, 0);
            break;
        case 1:
            debris->pos += Vector(-SPR, 0, 0);
            debris->velocity = Vector(-VEX, 0, 0);
            break;
        case 2:
            debris->pos += Vector(0, SPR, 0);
            debris->velocity = Vector(0, VEX, 0);
            break;
        case 3:
            debris->pos += Vector(0, -SPR, 0);
            debris->velocity = Vector(0, -VEX, 0);
            break;
        case 4:
            debris->pos += Vector(SPR, -SPR, SPR);
            debris->velocity = Vector(VEX, -VEX, VEX);
            break;
        }

        //debris->calc_cr();
        debris->cR *= 0.3;
        debris->scale *= 0.3;
#ifdef CM_MESH
        for (int i = 0; i < debris->cm.source.size(); i++) {
            (*debris->cm.source[i]) *= 0.3;
        }
#endif

        stuff.push_back(debris);
        update_collision_cache(debris);
    }
}


/******************************************************************************/
/* Sim */
/******************************************************************************/

Sim::Sim(Block *bp) : Stuff(bp), bullets(0.3, 100) {
    act[0] = act[1] = act[2] = 0;
    auto_heading = 2;

    gun_cooldown = 0;
    fire=false;
}

void Sim::tick(const float dt) {
    // collision recovery 
    if (collision_recovery > 0) {
        collision_recovery -= dt * 1000;
        if (collision_recovery <= 0) {
            auto_heading = 2;
            collision_recovery = 0;
        }
    }

    //
    // *** ship rotation ***
    //
    
    if (act[0]) rotvel.x += act[0] * ROT_ACCEL;
    else rotvel.x *= 0.8;
    if (act[1]) rotvel.y += act[1] * ROT_ACCEL;
    else rotvel.y *= 0.8;
    if (act[2]) rotvel.z += act[2] * ROT_ACCEL;
    else rotvel.z *= 0.8;
    if (rotvel.x > ROTVEL_CAP) rotvel.x = ROTVEL_CAP;
    if (rotvel.x < -ROTVEL_CAP) rotvel.x = -ROTVEL_CAP;
    if (rotvel.y > ROTVEL_CAP) rotvel.y = ROTVEL_CAP;
    if (rotvel.y < -ROTVEL_CAP) rotvel.y = -ROTVEL_CAP;
    if (rotvel.z > ROTVEL_CAP) rotvel.z = ROTVEL_CAP;
    if (rotvel.z < -ROTVEL_CAP) rotvel.z = -ROTVEL_CAP;

    _rot_axis = rotvel.get_normal();
    float w = rotvel.length();

    Quaternion dO(dt * w, _rot_axis);
    orientation = dO * orientation;
    orientation.normalize();


    //
    // *** now the linear motion ***
    //

    // calculate acceleration in the direction the ship is facing 
    // (Z axis of ship's orientation matrix)
    Vector right = front.cross(up);
    Vector a = front * thrust.z;

    // apply acceleration - update velocity
    velocity += a * dt * 90;
    if (thrust.z != 0) auto_speed = velocity.length();

    velocity += right * thrust.x;

    // auto-heading mode:
    // change velocity towards the direction the ship is facing
    if (auto_heading && (thrust.z == 0)) {
        Vector vn(velocity);
        vn.normalize();
        a = front - vn;
        velocity += a * dt * AUTO_HEADING_SCALE * auto_heading;

        float speed = velocity.length();
        if (auto_speed > speed + 1) velocity += front * dt * AUTO_HEADING_SCALE * auto_heading;
    }
    
    // apply velocity - update position
    move( velocity.x * dt, velocity.y * dt, velocity.z * dt );

    // shooting
    if (gun_cooldown > 0) gun_cooldown -= dt*1000;
    else if (fire) {
        gun_cooldown = GUN_COOLDOWN;
        Vector bv( front );
        bv.normalize();
        bv *= BULLET_SPEED;
        bv += velocity;
        bullets.add_particle(pos.x, pos.y, pos.z,
                             bv.x, bv.y, bv.z);
    }

    bullets.tick(dt);
}

/******************************************************************************/
/* Asteroid */
/******************************************************************************/

Asteroid::Asteroid(Stuff *blueprint) : Stuff(*blueprint) {
    // nop
    life = 15;
}

Asteroid::~Asteroid() {
    // nop
}

void Asteroid::randomize(unsigned int f) {
    if (f & R_ORIENTATION) {
        orientation *= Quaternion( rand() % 4, Vector::x_axis );
        orientation *= Quaternion( rand() % 4, Vector::y_axis );
        orientation *= Quaternion( rand() % 4, Vector::z_axis );
    }
    
    if (f & R_SCALE) {
        scale = (30 + rand() % 400) / 100.0;
        cR *= scale;
#ifdef CM_MESH
        for (int i = 0; i < cm_source.size(); i++) {
            (*cm_source[i]) *= scale;
        }
#endif
    }

    if (f & R_ROTATION) {
        if (rand() % 100 > 60) rotvel.x = 30 - (rand() % 60);
        if (rand() % 100 > 60) rotvel.y = 30 - (rand() % 60);
        if (rand() % 100 > 60) rotvel.z = 30 - (rand() % 60);
        rotvel.normalize();
    }

    if (f & R_VELOCITY) {
        if (rand() % 100 > 40) velocity.x = (60 - (rand() % 220)) / 10.0;
        if (rand() % 100 > 40) velocity.y = (60 - (rand() % 220)) / 10.0;
        if (rand() % 100 > 40) velocity.z = (60 - (rand() % 220)) / 10.0;
    }
}

